// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: donggu, jhkim

#pragma once

#include <algorithm>
#include <map>
#include <memory>
#include <string>
#include <tuple>
#include <vector>
#include <unordered_set>

#include "coin2/app/driver.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/log/strategy_logger.h"
#include "coin2/exchange/base/symbology/product.h"

#include "coin2/strategy/linear_model/lm_config.pb.h"
#include "coin2/strategy/linear_model/mm_util.h"
#include "coin2/strategy/strategy.h"
#include "coin2/strategy/util/latency_util.h"

#include "presto/quant/feature/feature.h"
#include "presto/quant/feature/feature_manager.h"
#include "presto/quant/feature/linear_model/linear_model.h"  // LinearModel, LinearModelDelegate
#include "presto/quant/feature/linear_model/linear_model_delegate_proto.h"

#include "presto/quant/math/ewma.h"

namespace coin2::strategy::linear_model {

using ::coin2::exchange::base::symbology::ProductEncyclopedia;
using ::coin2::exchange::base::feed::FeedUpdate;
using ::coin2::exchange::base::strategy_util::StrategyLogger;
using ::coin2::exchange::base::symbology::IProduct;
using ::presto::Symbol;
using ::presto::SymbolManager;
using ::presto::quant::feature::linear_model::LinearModel;
using ::presto::quant::feature::linear_model::LinearModelDelegateProto;

class ProductLmStrategy;

namespace {

inline coin::proto::LatencyProto::LatencyTag GetFeedLatencyTag(const FeedUpdate& upd) {
  if (upd.is_book())
    return coin::proto::LatencyProto::BOOK_FEED_LATENCY;
  else if (upd.is_trade())
    return coin::proto::LatencyProto::TRADE_FEED_LATENCY;
  else
    return coin::proto::LatencyProto::FEED_LATENCY;
}

inline coin::proto::AccountRequestProto GetAccRequestFromMea(
    const MarketExchangeApi& mea) {
  coin::proto::AccountRequestProto acc_request;
  acc_request.set_market_type(MarketType_Name(mea.market));
  acc_request.set_exchange(ExchangeType_Name(mea.exchange));
  acc_request.set_api_version(mea.api);
  return acc_request;
}

inline coin::proto::AccountRequestProto GetAccRequestFromFeed(
    const FeedUpdate& upd) {
  return GetAccRequestFromMea(upd.mea());
}

}  // namespace

struct SymbolSignalLogger {
  const int64_t kSigFlushFreq = 5 * 60e9;
  int64_t last_stat_flush_ts;
  std::vector<presto::math::MovingAverageWindow<double>> signals;
  presto::math::MovingAverageWindow<double> comb_signal;

  void LogSignal(
      int64_t timestamp,
      ::presto::quant::feature::linear_model::LinearModelDelegateProto* delegate,
      coin2::exchange::base::strategy_util::StrategyLogger* strat_logger,
      const std::string& symbol_str) {
    if (last_stat_flush_ts == 0) {
      last_stat_flush_ts = timestamp;
    }
    auto signal_resp = delegate->response_.buy_model().Get(0);
    int n_models = delegate->response_.buy_model().size();
    if (signals.size() == 0) {
      comb_signal.ResizeWindow(kSigFlushFreq);
    }
    while (signals.size() < n_models) {
      signals.push_back(presto::math::MovingAverageWindow<double>(kSigFlushFreq));
    }
    double buymodel_signal = 0;
    for (int i = 0; i < n_models; i++) {
      buymodel_signal += delegate->response_.buy_model().Get(i).signal();
      signals[i].Update(timestamp, delegate->response_.buy_model().Get(i).signal());
    }
    buymodel_signal /= n_models;
    comb_signal.Update(timestamp, buymodel_signal);

    if (last_stat_flush_ts + kSigFlushFreq < timestamp) {
      nlohmann::json combstat;
      for (int i = 0; i < signals.size(); ++i) {
        nlohmann::json eachstat;
        eachstat["n"] = signals[i].num_elements();
        eachstat["mean"] = signals[i].average();
        eachstat["std"] = signals[i].simplestd();
        combstat[fmt::format("{}", i)] = eachstat;
      }
      {
        nlohmann::json eachstat;
        eachstat["n"] = comb_signal.num_elements();
        eachstat["mean"] = comb_signal.average();
        eachstat["std"] = comb_signal.simplestd();
        combstat["comb"] = eachstat;
      }
      combstat["window_size"] = timestamp - last_stat_flush_ts;
      combstat["timestamp"] = timestamp;
      combstat["tag"] = "SIGNAL_INFO";
      combstat["symbol"] = symbol_str;
      strat_logger->WriteExtraInfo(fmt::format("{}", combstat.dump()), true, timestamp);
      signals.clear();
      comb_signal.clear();
      last_stat_flush_ts = timestamp;
    }
  }
};


struct LogicHolder {
  LogicHolder(
      const MarketExchangeApi& mea,
      const IProduct& target_product,
      const Symbol* target_symbol,
      ProductEncyclopedia* product_cache)
      : mea(mea),
        target_product(target_product),
        target_symbol(target_symbol),
        product_cache(product_cache) {}

  bool IsQuotePosOutdated(int64_t timestamp) {
    return last_manage_quotesize_ts + 600'000'000'000LL < timestamp;
  }

  std::unordered_map<std::string, SymbolSignalLogger> signal_logger;
  void LogSignal(
      int64_t timestamp,
      ::presto::quant::feature::linear_model::LinearModelDelegateProto* delegate) {
    if (delegate && !delegate->response_.buy_model().empty()) {
      auto signal_resp = delegate->response_.buy_model().Get(0);
      auto symbol_str = signal_resp.symbol();
      signal_logger[symbol_str].LogSignal(timestamp, delegate, strat_logger, symbol_str);
    }
  }

  void ManageQuoteSize(double midp, int64_t timestamp) {
    if (prod_conf_ptr && IsQuotePosOutdated(timestamp)) {
      double max_pos = GetQtyFromQuote(
          *product_holder, midp, prod_conf_ptr->qty().max_position_quote());
      double min_pos = GetQtyFromQuote(
          *product_holder, midp, prod_conf_ptr->qty().min_position_quote());
      double lot_size = GetQtyFromQuote(
          *product_holder, midp, prod_conf_ptr->qty().max_lot_size_quote());
      double min_order_qty = GetQtyFromQuote(
          *product_holder, midp, prod_conf_ptr->qty().min_order_quote());

      std::optional<double> maintain_qty;
      if (prod_conf_ptr->side().maintain_quote() > 0) {
        maintain_qty = GetQtyFromQuote(
            *product_holder, midp, prod_conf_ptr->side().maintain_quote());
      }

      ManageExecutorConfig(lot_size, min_pos, max_pos, timestamp);
      oe->AdjustSize(
          *product_holder,
          min_pos,
          max_pos,
          lot_size,
          min_order_qty,
          maintain_qty);
      CHECK(is_quote_pos);
      SPDLOG_INFO("Reserve Check: {}", config.ShortDebugString());
    }
  }

  void ManageExecutorConfig(double lot_size, double min_pos, double max_pos, int64_t timestamp) {
    // dont change reserve too often
    if (IsQuotePosOutdated(timestamp)) {
      config.set_lot_size(lot_size);
      config.set_min_pos(min_pos);
      config.set_max_pos(max_pos);
      last_manage_quotesize_ts = timestamp;
      quote_pos_updated = true;
    }
  }

  void InitExecutor(
      ::coin2::strategy::order_executor::OrderExecutorSystem* oe,
      coin2::exchange::base::strategy_util::StrategyLogger* strat_logger,
      bool enable_telemetry,
      int64_t curr_time,
      bool is_focus = false) {
    CHECK(oe->mea() == this->mea);
    this->strat_logger = strat_logger;
    this->oe = oe;
    this->enable_telemetry = enable_telemetry;
    this->init_executor = true;
    if (!is_focus) {
      for (auto& [prod, prod_conf] : oe->config().products()) {
        auto products = ProductRequestsFromConfig(
            oe->mea().String(), prod_conf.products(), curr_time, false);
        if (!products.empty()) {
          CHECK_EQ(products.size(), 1);
        } else {
          std::unique_ptr<IProduct> product = CreateProductFromNormString(
              oe->mea(),
              prod,
              curr_time,
              false);
          if (product) {
            products.push_back(std::move(product));
          } else {
            continue;
          }
        }
        if (target_product.relative_norm() == products[0]->relative_norm()) {
          config.Clear();
          config.set_symbol(target_product.absolute_norm());
          if (prod_conf.qty().has_max_position_quote()) {
            prod_conf_ptr = &prod_conf;
            is_quote_pos = true;
          } else {
            config.set_lot_size(prod_conf.qty().max_lot_size());
            config.set_min_pos(prod_conf.qty().min_position());
            config.set_max_pos(prod_conf.qty().max_position());
          }
        }
      }
    } else {
      config.Clear();
    }
    if (enable_telemetry) {
      SPDLOG_INFO("Set Latency Recorder");
      oe->order_system()->gateway()->set_latency_recorder(
          strat_logger->mutable_latency_recorder());
    }
  }

  void InitAccReq(const coin::proto::AccountRequestProto& orig_acc_req) {
    CHECK(!acc_req.has_value());
    acc_req = orig_acc_req;
  }

  void TriggerModelWithLatencyRecording(LinearModel* linear_model, const Symbol* symbol) {
    if (strat_logger) {
      strat_logger->before_evaluation_time = GetCurrentTimestamp();
    }
    linear_model->TriggerAllSymbolWithOrder(symbol);
    if (strat_logger) {
      strat_logger->after_evaluation_time = GetCurrentTimestamp();
    }
  }

  void WriteTelemetryImpl(const FeedUpdate& upd) {
    /* --------------------------- set latency telemetry ------------------ */
    if (enable_telemetry && *enable_telemetry) {
      auto acc_request = GetAccRequestFromFeed(upd);
      auto feed_latency_tag = GetFeedLatencyTag(upd);
      coin2::strategy::util::SetFeedTimeToLatency(
          strat_logger,
          feed_mgr,
          upd,
          feed_latency_tag,
          acc_request,
          true,
          true);
      coin2::strategy::util::SetFeedTimeToLatency(
          strat_logger,
          feed_mgr,
          upd,
          coin::proto::LatencyProto::ORDER_LATENCY,
          acc_request,
          false,
          true);
      strat_logger->clear_times();
    }
  }

  void UpdateFeed(
      const Symbol* symbol,
      const FeedUpdate& upd,
      const ::fastfeature::FeedMessage& feed_msg) {
    last_feed_timestamp = upd.timestamp();
    if (!product_holder) {
      product_holder = &(product_cache->holder(target_product));
    }
    if (upd.product() == target_product && upd.is_book()) {
      last_book = &upd.book();
    }
    oe->SetSimTimestamp(*product_holder, upd.timestamp());
    if (HasBookAndPos()) {
      auto ask0p = last_book->Ask0()->price;
      auto bid0p = last_book->Bid0()->price;
      double midp = 0.5 * (ask0p + bid0p);
      if (prod_conf_ptr) {
        ManageQuoteSize(midp, feed_msg.fetched_time());
      }

      /* --------------------------- print order status --------------------- */
      SPDLOG_INFO_EVERY_NS(
          200'000'000'000L,
          upd.timestamp(),
          "{} - {}\n{}",
          mea.String(),
          target_product.symbol(),
          oe->DebugString(upd.timestamp()));
    }
  }

  std::optional<double> GetMidPrice() const {
    if (last_book == nullptr || !last_book->Ask0() || !last_book->Bid0()) {
      return std::nullopt;  // no book or ask0 or bid0
    }
    return (last_book->Ask0()->price + last_book->Bid0()->price) * 0.5;
  }

  void UpdateMidp(
      StrategyReporter2* strat_reporter,
      const IProduct* target_product_given,
      std::optional<bool> has_reserve = std::nullopt,
      bool is_focus = false) {
    if (!init_executor) return;
    if (!has_reserve.has_value() && mea.market == MarketType::Futures) {
      has_reserve = false;
    }
    auto curr_time = GetCurrentTimestamp();
    bool is_flexible_pos = is_focus || is_quote_pos;
    if (!executor_config_registered || (is_flexible_pos && quote_pos_updated && acc_req.has_value())) {
      coin2::strategy::PassiveExecutorConfig exe_config;
      if (is_focus) {
        exe_config.set_symbol(target_product_given->absolute_norm());
      } else {
        exe_config.set_symbol(config.symbol());
      }
      exe_config.set_lot_size(config.lot_size());
      exe_config.set_min_pos(config.min_pos());
      exe_config.set_max_pos(config.max_pos());
      if (has_reserve.has_value() && *has_reserve == false) {
        exe_config.set_reserve(0);
      }
      strat_reporter->RegisterExecutorConfig(
          curr_time, acc_req.value(), *target_product_given, exe_config);
      executor_config_registered = true;
      quote_pos_updated = false;
    }
  }

  bool HasBookAndPos() const {
    if (!oe->HasPosition(*product_holder)) return false;
    if (last_book == nullptr) return false;
    if (!last_book->Ask0() || !last_book->Bid0()) return false;
    return true;
  }

  const coin2::feed::mgr::impl::FeedSubsystemManager* feed_mgr{nullptr};
  const MarketExchangeApi mea;
  const IProduct& target_product;
  const Symbol* target_symbol;
  ProductEncyclopedia* product_cache;
  std::optional<bool> enable_telemetry;

  // init on InitExecutor()
  bool init_executor = false;
  coin2::exchange::base::strategy_util::StrategyLogger* strat_logger = nullptr;
  ::coin2::strategy::order_executor::OrderExecutorSystem* oe = nullptr;
  coin::proto::coin_executor::AggressiveExecutorConfig config;

  // init on InitAccReq()
  std::optional<coin::proto::AccountRequestProto> acc_req;

  // init during UpdateFeed()
  const coin2::exchange::base::symbology::IProductHolder* product_holder = nullptr;
  const IBook* last_book = nullptr;

  // mutables
  mutable int64_t last_feed_timestamp = 0L;
  mutable int64_t prev_executor_config_ts = 0L;

  // quotesizer
  int64_t last_manage_quotesize_ts = 0L;
  bool is_quote_pos = false;
  bool quote_pos_updated = false;
  bool executor_config_registered = false;
  const OrderExecutorConfig* prod_conf_ptr = nullptr;
};

class LinearModelLogic {
 public:
  LinearModelLogic(
      const MarketExchangeApi& mea,
      const IProduct& target_product,
      const Symbol* target_symbol,
      ProductEncyclopedia* product_cache)
      : lholder_(mea, target_product, target_symbol, product_cache) {}
  virtual ~LinearModelLogic() = default;
  virtual void onFeed(
      const Symbol* symbol,
      const FeedUpdate& upd,
      const ::fastfeature::FeedMessage& feed_msg,
      LinearModel* linear_model,
      int64_t* lm_feed_id,
      const int64_t* global_feed_id,
      LinearModelDelegateProto* delegate,
      bool simulation) = 0;
  virtual void InitImpl(::coin2::app::Driver* driver, StrategyLogger* strat_logger) = 0;
  virtual void Init(::coin2::app::Driver* driver,
                    StrategyLogger* strat_logger,
                    const coin::proto::AccountRequestProto& acc_req) {
    lholder_.InitAccReq(acc_req);
    InitImpl(driver, strat_logger);
  }
  void SetFeedMgr(const coin2::feed::mgr::impl::FeedSubsystemManager* feed_mgr) {
    lholder_.feed_mgr = feed_mgr;
  }
  void WriteTelemetry(const FeedUpdate& upd) {
    lholder_.WriteTelemetryImpl(upd);
  }
  virtual void AddConfig(
      const IProduct& target_product,
      const Symbol* target_symbol,
      const LmProductConfig& lm_logic_config) {
    THROW() << "not implemented for this cls";
  }
  void SetStartTimestamp(int64_t timestamp_override) {
    timestamp_override_ = timestamp_override;
  }
  void SetProductStrategy(ProductLmStrategy* product_strategy) {
    product_strategy_ = product_strategy;
  }
  virtual void UpdateMidp(
      StrategyReporter2* strat_reporter,
      const IProduct* target_product_given) {
    strat_reporter->UpdateMidp(
        *target_product_given,
        GetMidPrice());
  }

  void UpdateModelFeedOnly(
      const Symbol* symbol,
      const ::fastfeature::FeedMessage& feed_msg,
      int64_t* lm_feed_id,
      const int64_t* global_feed_id,
      LinearModel* linear_model) {
    if (*lm_feed_id < *global_feed_id) {
      linear_model->UpdateFeed(symbol, feed_msg);
      *lm_feed_id = *global_feed_id;
    }
  }

  /* -------------------------------------------------------------------------- */
  /*                               strategy state                               */
  /* -------------------------------------------------------------------------- */
  std::optional<double> GetMidPrice() const {
    return lholder_.GetMidPrice();
  }
  virtual void OnFill(
      int64_t timestamp,
      coin::proto::TradeSide trade_side,
      double fill_price,
      double fill_qty) {}

  virtual void onAccountInfo(const coin2::exchange::base::order::OrderUpdate& upd) {}
  virtual void onAccountOrder(const coin2::exchange::base::order::OrderUpdate& upd) {}

  virtual void TryClear(
      const Symbol* symbol,
      const FeedUpdate& upd,
      const ::fastfeature::FeedMessage& feed_msg) {
    if (symbol != lholder_.target_symbol) return;
    lholder_.UpdateFeed(symbol, upd, feed_msg);
    if (!lholder_.HasBookAndPos()) return;
    if (feed_msg.fetched_time() - prev_manage_time_ < 3'000'000'000L) return;
    prev_manage_time_ = feed_msg.fetched_time();
    lholder_.oe->CancelAllOrders(
        *lholder_.product_holder, OrderDirection::DIRECTION_SELL, feed_msg.fetched_time());
    lholder_.oe->CancelAllOrders(
        *lholder_.product_holder, OrderDirection::DIRECTION_BUY, feed_msg.fetched_time());
    auto ask0p = lholder_.last_book->Ask0()->price;
    auto bid0p = lholder_.last_book->Bid0()->price;
    if (ask0p == 0 || bid0p == 0) return;

    double current_pos = lholder_.oe->GetRelativePosition(*lholder_.product_holder);
    double lot_size = 0.5 * lholder_.oe->GetConfig(*lholder_.product_holder).qty().max_lot_size();
    if (current_pos > 0) {
      lot_size = std::min(lot_size, current_pos);
      double rounded_price =
          lholder_.product_holder->product_info().tick_price().RoundPassively(
              ask0p, OrderDirection::DIRECTION_SELL);
      if (rounded_price <= 0) return;
      lholder_.oe->SubmitOrder(
          *lholder_.product_holder,
          rounded_price,
          lot_size,
          OrderDirection::DIRECTION_SELL,
          OrderDuration::GTC_ORDER,
          OrderType::LIMIT_ORDER,
          ask0p,
          10,  // leverage rate
          true,  // post_only
          "clear",
          feed_msg.fetched_time(),
          false,
          false);
    } else if (current_pos < 0) {
      lot_size = std::min(lot_size, -current_pos);
      double rounded_price =
          lholder_.product_holder->product_info().tick_price().RoundPassively(
              bid0p, OrderDirection::DIRECTION_BUY);
      if (rounded_price <= 0) return;
      lholder_.oe->SubmitOrder(
          *lholder_.product_holder,
          rounded_price,
          lot_size,
          OrderDirection::DIRECTION_BUY,
          OrderDuration::GTC_ORDER,
          OrderType::LIMIT_ORDER,
          bid0p,
          10,  // leverage rate
          true,  // post_only
          "clear",
          feed_msg.fetched_time(),
          false,
          false);
    }
  }

  void SetClearProductNames(
      const std::unordered_set<std::string>& clear_product_names) {
    clear_product_names_ = clear_product_names;
  }

  bool BanUpdatingFeatureFeed(const Symbol* symbol) const {
    return clear_product_names_.count(symbol->product_name()) > 0;
  }

 protected:
  ProductLmStrategy* product_strategy_ = nullptr;
  LogicHolder lholder_;
  int64_t prev_manage_time_ = 0L;
  int64_t timestamp_override_ = 0L;
  std::unordered_set<std::string> clear_product_names_;
};

}  // namespace coin2::strategy::linear_model
